在上一篇暖身文章中大致聊過了一些基本知識,像是運算子、運算式、值與型別、變數、條件式、迴圈,本文還會再探討一些基礎概念,像是
本文也僅是概念而已,之後會有單篇章節細細討論的,所以就算是暖身 Part 2 摟。
開始吧!
這個部份要來談關於變數的存取規則,例如:範疇、拉升等。
函式會建立自己的範疇,其內的識別字(不管是變數、函式)僅能在這個函式裡面使用。
如下,在全域範疇底下,是無法存取 foo 內的 a、b、c 和 bar,否則會導致 ReferrenceError;但在 foo 自己的函式範疇內,可以存取 a、b、c 和 bar。
function foo(a) {
var b = 2;
function bar() {
// ...
}
var c = 3;
}
console.log(a); // ReferrenceError
console.log(b); // ReferrenceError
console.log(c); // ReferrenceError
bar(); // ReferrenceError
在程式執行前,編譯器(compiler)會先由上到下逐行將程式碼轉為電腦可懂的命令,然後再執行編譯後的指令。在這個編譯的階段,編譯器找出所有的變數並繫結所屬範疇,但不賦值,所以此刻變數所帶的值是 undefined;而在執行階段,JavaScript 引擎才會處理給值的事情。
我們可以把這個過程想像成是將這些變數「提升」到程式碼的最頂端,如下範例所示,因此當印出 a 的值的時候,會是已宣告但還沒賦值的狀態,也就是有這個變數,但其值是 undefined,一直到程式執行了,才給值。因此,我們可以在程式碼任何地方呼叫運用這個變數,但只有在正式宣告之後才能有正確的值可用,在宣告之前使用都會得到 undefined。
var a; // 編譯時期的工作
console.log(a); // undefined
a = 2; // 執行時期的工作
若在目前執行的範疇找不到這個變數的時候,就會往外層的範疇搜尋,持續搜尋直到找到為止,或直到最外層的全域範疇(global scope,在瀏覽器底下就是指 window)。
如下,console.log(a + b)
中,b 無法在 foo 中找到,但可從全域範疇中追出來。
const foo = (a) => {
console.log(a + b);
}
const b = 2;
foo(2); // 4
相較於巢狀範疇是以函式為劃分單位,區塊範疇就是以大括號為界線了。
嚴格模式簡單說就是為了預防開發者的一些不小心或錯誤的行為,JavaScript 引擎協助做了一些檢測的工作,當開發者誤用時就把錯誤丟出來。可參考-MDN。
範例如下,在未宣告變數而賦值的狀況下,會無預警的產生一個全域變數,但若使用嚴格模式('use strict'
)則會禁止這行為外,還會報錯,告知開發者變數尚未被定義。
'use strict';
a = 1; // Uncaught ReferenceError: a is not defined
就把它想像成是一個諄諄教誨的好老師吧!總是願意告訴你殘忍的實話...
這標題看起來有點怪怪的(?)但其實也只是要說明,函式本身就和其他的值一樣,是可以被指定給某個變數、當參數傳遞或當成其他函式的回傳值。記得,函式也只是物件的子型別而已,沒什麼特別的。
指定給某個變數,如下,指定給 foo。
var foo = function() {
console.log('大家好,我是 foo!');
}
當參數傳遞,如下,將 foo 當成是 bar 的參數傳入。
var foo = function() {
console.log('大家好,我是 foo!');
}
function bar(func) {
func();
}
bar(foo); // 大家好,我是 foo!
當成其他函式的回傳值,foo 是 baz 的回傳值,並將結果指定給 result。
var foo = function() {
console.log('大家好,我是 foo!');
}
var result = function baz(func) {
return func;
}
result(foo)(); // 大家好,我是 foo!
因此,這個函式值(例如:var foo = function() { ... }
)也可被視為是一個運算式,就稱呼它為「函式運算式」吧。之後還會提到函式宣告、函式運算式與匿名 vs 具名,待後續更詳細的說明。
IIFE 是為可立即執行的函式運算式。一般的函式運算式並不會馬上執行,若要執行除了在其名稱後加上小括號外,還可以利用 IIFE 的方式執行它,匿名或具名皆合法。使用 IIFE 的好處主要是不污染全域範疇。
範例如下,這是一個匿名的 IIFE,a 在全域範疇是找不到的。
(function() {
var a = 3;
console.log(a); // 3
})();
// 不污染全域範疇
a // Uncaught ReferenceError: a is not defined
閉包是指變數的生命週期只存在於該函式內,一旦離開了函式,該變數就會被回收而不可再利用,且必須在函式內事先宣告。
範例如下,在函式 closure 內可以存取 a 的值,但離開了函式 closure 走到全域範疇之下,就取不到 a 的值了,因此會被報錯「Uncaught ReferenceError: a is not defined」。
function closure() {
var a = 1;
console.log(a); // 1
}
closure();
a // Uncaught ReferenceError: a is not defined
模組模式(Module Pattern)又稱為揭露模組(Revealing Module),經由建立一個模組實體(Module Instance,如下範例的 foo),來調用內層函式。而內層函式由於具有閉包的特性,因此可存取外層包含函式(Outer Enclosing Function)之內的變數和函式。透過模組模式,可隱藏私密的資訊,並對外公開 API。
範例如下,CoolModule 對外公開 API doSomething 和 doAnother,CoolModule 之外是無法取得其私有的 something 和 another 兩個變數的值。
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
this 到底是指向誰一直都是個令人費解的問題。
圖片來源:What is this meaning of this?
簡單來說,this 是 function 執行時所屬的物件,而 this 是在執行時期做繫結,其值和函式在哪裡被呼叫(call-site)有關。
總結規則如下,並以匹配的優先順序由高至低排列
範例如下。
function foo() {
console.log(this.bar);
}
var bar = 'global';
var obj1 = {
bar: 'obj1',
foo: foo
};
var obj2 = {
bar: 'obj2'
};
foo(); // 'global'
obj1.foo(); // 'obj1'
foo.call( obj2 ); // 'obj2'
new foo(); // undefined
原型可說是物件的一種 fallback 機制,當在此物件找不到指定屬性時,就會透過原型鏈結(prototype link / prototype reference)追溯到其父物件上。範例如下,若想存取 bar.a
但由於 bar 並無 a 屬性,因此就會透過原型鏈結找到 foo,並得到 100 這個值。
var foo = { a: 100 };
var bar = Object.create(foo); // 建立 bar 物件,並連結到 foo
bar.b = 'hi';
bar.a // 100,委派給 foo
bar.b // 'hi'
另外,原型最常應用於「行為委派」(behavior delegation),如上例所示,將物件 bar 的行為委派給 foo,這也是常聽到很類似於其他語言的類別的繼承功能,但其實完全不同。
面對新舊功能並存的狀況要怎麼處理呢?這裡要介紹兩種方法-Polyfill 和 Transpiler。
Polyfilling 的意思就是依據一個新功能的定義,製作具有相同行為,而能在較舊的 JavaScript 環境執行的程式碼,白話說就是為舊瀏覽器掛載新功能。
這裡來看一個例子,針對 isNaN 的改進...
NaN 表示值為無效的數字,它會產生的原因是
以上就會產生 NaN。
在 ES6 以前,開發者使用 isNaN
在數學運算或解析字串後檢測得到的結果是否為合法的數字,其實就是檢測是否為 NaN,其過程為先將輸入值使用 Number 強制轉型為數字,若無法轉為有效的數字而得到 NaN 時就判定等於 NaN,結果得到 true。
範例如下,空物件 {}
經過 isNaN 判斷是 NaN,意即不為數字。
isNaN({}) // true,不是數字
// 拆解詳細過程如下...
Number({}) // 先將空物件轉為數字,得到 NaN
isNaN(NaN) // 檢查是否為 NaN,得到 true
其他範例還有...
isNaN(123) // false
isNaN(-1.23) // false
isNaN(5-2) // false
isNaN(0) // false
isNaN('123') // false
isNaN('Hello World') // true
isNaN('2000/01/01') // true
isNaN('') // false
isNaN(true) // false
isNaN(undefined) // true
isNaN('NaN') // true
isNaN(NaN) // true
isNaN(0/0) // true
isNaN(1/0) // false
但這檢測方式的常常會讓開發者得到讓人容易誤解的結果(像是空物件 {}
就真的不等於 NaN 呀),因此 ES6 推出了 Number.isNaN
,Number.isNaN
不會經過轉為數字的這個過程,而是直接判斷型別是否為數字且是否等於 NaN。承上範例,檢測空物件 {}
是否為 NaN,得到 false。
Number.isNaN({}) // 直接檢查空物件是否為 NaN,得到 false
同樣也來看剛才的範例...
Number.isNaN(123) // false
Number.isNaN(-1.23) // false
Number.isNaN(5-2) // false
Number.isNaN(0) // false
Number.isNaN('123') // false
Number.isNaN('Hello World') // false
Number.isNaN('2000/01/01') // false
Number.isNaN('') // false
Number.isNaN(true) // false
Number.isNaN(undefined) // false
Number.isNaN('NaN') // false
Number.isNaN(NaN) // true
Number.isNaN(0/0) // true
Number.isNaN(1/0) // false
雖然 ES6 出了這個新功能,但不見得所有的瀏覽器都會支援,因此對於較舊瀏覽器,就掛個 polyfill 來模擬這個新功能。
polyfill 如下。
if (!Number.isNaN) {
Number.isNaN = function isNaN(x) {
return x !== x; // NaN 是唯一一個不等於自己的值
};
}
ES6 定義了常數 Number.NaN
來表示 NaN。
isNaN(NaN); // true
isNaN(Number.NaN); // true
Number.isNaN(Number.NaN) // true
Number.isNaN(NaN) // true
由於實作 polyfill 難免會有缺漏或疏失,這裡提供兩個經過嚴格審核的函式庫以供使用-es5-shim 和 es6-shim。
並非所有的新功能都能經由 polyfill 掛載到舊環境上,這裡提出另一個解法-將帶有新功能的程式碼轉換成等效的舊程式碼就可以了,也就是使用 transpiler 做轉譯。
例如,ES6 推出了新的功能「預設參數值」。
function foo(a = 2) {
console.log(a);
}
foo(); // 2
foo(42); // 42
但這在舊的 JavaScript 引擎中是無效的,因此 transpiler 就會將以上程式碼變形、翻譯成等效的舊程式碼。
function foo() {
var a = arguments[0] !== (void 0) ? arguments[0] : 2;
console.log(a);
}
這麼做的好處是在開發階段開發者依然能享受新功能帶來的好處,但又能兼顧到新舊瀏覽器的狀況。這裡也推薦一些很棒的 transpiler,像是 Babel、Traceur 等。
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...
暖身結束,接下來就要進入正題了,明天見!
同步發表於部落格。